﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Reflection;
using System.ComponentModel;
using SwiftMVVM.ChangeTracking;
using System.Collections;

namespace SwiftMVVM.ViewModel
{
    /// <summary>
    /// DependentBindingObject provides an attribute based approach
    /// to Automatically call INotifyPropertyChanged.PropertyChanged
    /// on Properties on the current object when properties you depend on 
    /// are changed. 
    /// </summary>
    public abstract class DependentBindingObject : BindingObject
    {
        /// <summary>
        /// Created mostly to add readability to the code. This class 
        /// encapsulates the creation of the notification and dependency mappings.
        /// This class should not be consumed externally
        /// </summary>
        private sealed class DependencyNotifications
        {
            /// <summary>
            /// Contains a list of properties and the properties that will get effected
            /// </summary>
            public readonly Dictionary<string, List<KeyValuePair<Collection<PropertyInfo>, string>>> Dependencies =
                new Dictionary<string, List<KeyValuePair<Collection<PropertyInfo>, string>>>();

            /// <summary>
            /// Constructs a new set of mappings for the type
            /// </summary>
            /// <param name="type"></param>
            public DependencyNotifications(Type type)
            {
                //Check all the properties for the DependsOn attribute
                foreach (var propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
                {
                    foreach (var dependsOnAttribute in (DependsOnAttribute[])propertyInfo.GetCustomAttributes(typeof(DependsOnAttribute), true))
                    {
                        if (propertyInfo.CanWrite) { throw new InvalidOperationException("Properties that are marked with DependsOn must be ReadOnly"); }

                        Collection<PropertyInfo> propertyInfoCollection = null;
                        if (!string.IsNullOrWhiteSpace(dependsOnAttribute.Source))
                        {
                            propertyInfoCollection = new Collection<PropertyInfo>();
                            var currentType = type;
                            foreach (var pathPart in dependsOnAttribute.Source.Split("./".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
                            {
                                var sourcePropertyInfo = currentType.GetProperty(pathPart);
                                if (sourcePropertyInfo == null) { throw new InvalidOperationException("Property does not exist on this object"); }

                                currentType = sourcePropertyInfo.PropertyType;
                                propertyInfoCollection.Add(sourcePropertyInfo);
                            }

                            if (currentType != null)
                            {
                                if (!typeof(INotifyPropertyChanged).IsAssignableFrom(currentType))
                                {
                                    throw new InvalidOperationException(
                                        string.Format("Can only Depend on properties that exist on " +
                                        "objects derived from INotifyPropertyChanged. {0} does " +
                                        "not derive from INotifyPropertyChanged.", currentType.Name));
                                }
                            }
                        }
                        else
                        {
                            if (!typeof(INotifyPropertyChanged).IsAssignableFrom(propertyInfo.ReflectedType))
                            {
                                throw new InvalidOperationException(
                                        string.Format("Can only Depend on properties that exist on " +
                                        "objects derived from INotifyPropertyChanged. {0} does " +
                                        "not derive from INotifyPropertyChanged.", propertyInfo.ReflectedType.Name));
                            }
                        }

                        List<KeyValuePair<Collection<PropertyInfo>, string>> dependencies;
                        if (!Dependencies.TryGetValue(dependsOnAttribute.Property, out dependencies))
                        {
                            Dependencies.Add(dependsOnAttribute.Property,
                                dependencies = new List<KeyValuePair<Collection<PropertyInfo>, string>>());
                        }
                        dependencies.Add(new KeyValuePair<Collection<PropertyInfo>, string>(propertyInfoCollection, propertyInfo.Name));
                    }
                }
            }
        }

        /// <summary>
        /// Locking object used to synchronize access to the event args cache
        /// </summary>
        private static readonly object lockObject = new Object();

        /// <summary>
        /// For performance reasons we want to store dependency mappings by type
        /// </summary>
        private static readonly Dictionary<Type, DependencyNotifications> typeMappings = new Dictionary<Type, DependencyNotifications>();

        /// <summary>
        /// If we have Properties that depend on us, this will fire after the notifications are sent
        /// </summary>
        public event EventHandler<PropertyChangedEventArgs> PropertyNotificationsSent;

        /// <summary>
        /// Backing field for the UseChangeMonitor property
        /// </summary>
        private bool useChangeMonitor;

        /// <summary>
        /// Gets whether or not this object is responsible for brokering 
        /// PropertyChange notifications across child objects.
        /// This needs to be True on the root object only if you have 
        /// dependencies between the root and child DependantObjects
        /// </summary>
        public bool UseChangeMonitor
        {
            get { return this.useChangeMonitor; }
            set
            {
                if (SetPropertyValue(() => { }, ref this.useChangeMonitor, value))
                {
                    if (value)
                    {
                        this.notifyChangeMonitor = new NotifyChangeMonitor();
                        this.notifyChangeMonitor.MonitoredObjectPropertyChanged += new EventHandler<PropertyChangeEventArgs>(notifyChangeMonitor_MonitoredObjectPropertyChanged);
                        this.notifyChangeMonitor.VerifyMonitorObjectProperty += new EventHandler<VerifyMonitorObjectPropertyEventArgs>(ChangeManager.NotifyChangeMonitor_VerifyMonitorObjectProperty);
                        this.notifyChangeMonitor.StartMonitor(this);
                    }
                    else
                    {
                        if (this.notifyChangeMonitor != null)
                        {
                            this.notifyChangeMonitor.StopMonitor(this);
                            this.notifyChangeMonitor = null;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// We use the NotifyChangeMonitor to monitor all changes to the object
        /// we're managing changes on. It will notify us when items are added/removed
        /// from collections as well as when properties change on given objects
        /// </summary>
        private NotifyChangeMonitor notifyChangeMonitor;

        /// <summary>
        /// Constructs a new DependentBindingObject and on the first type creation
        /// goes ahead and determines mapping notifications for this type
        /// </summary>
        /// <param name="notifySubObjectChanges">
        /// Gets whether or not this object is responsible for brokering 
        /// PropertyChange notifications across child objects.
        /// This needs to be True on the root object only if you have 
        /// dependencies between the root and child DependantObjects.
        /// This can only be true on the Root object as there is no need
        /// to have more than one ChangeMonitor
        /// </param>
        public DependentBindingObject()
        {
            //Check to see if we've mapped this type already.
            //For external property notifications we're going to cache propertyinfo 
            //to allow us to resolve the external sender quickly
            typeMappings.GetOrAdd(lockObject, this.GetType(), key =>
            {
                return new DependencyNotifications(key);
            });
        }

        /// <summary>
        /// Fires after Property Notifications are sent to Dependent Binding Objects
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="propertyName"></param>
        protected virtual void OnPropertyNotificationsSent(string propertyName)
        {
            var handler = this.PropertyNotificationsSent;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// We'll only get in here is UseChangeMonitor is true. By setting UseChangeMonitor
        /// to true we can support Depending on external object properties. Like a subitem or parent
        /// property change
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void notifyChangeMonitor_MonitoredObjectPropertyChanged(object sender, PropertyChangeEventArgs e)
        {
            //Make sure we only have one change monitor active.
            var activeChangeMonitorCount = 0;

            //Send the PropertyChanging and PropertyChanged to all monitored 
            //DependentBindingObjects that are interested in it.
            foreach (var item in MonitoredDependentBindingObjects)
            {
                activeChangeMonitorCount += item.UseChangeMonitor ? 1 : 0;
                if (activeChangeMonitorCount > 1) throw new InvalidOperationException(
                    "Only one object in the graph, preferably the root, can monitor/broker property changes across objects.");

                if (item != e.SourceObject)
                {
                    //Notify the item that a property on an external object
                    //has changed. This will allow him to figure out if he
                    //wants to deal with it.
                    item.ExternalObjectPropertyChanged(e.SourceObject, e.PropertyName);
                }
            }

            OnPropertyNotificationsSent(e.PropertyName);
        }

        /// <summary>
        /// This is where we handle external property change notifications
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="propertyName"></param>
        private void ExternalObjectPropertyChanged(object sender, string propertyName)
        {
            //Another object has fired a property change event
            //Check all of our mappings to see if we're listening to the 
            //property named.
            List<KeyValuePair<Collection<PropertyInfo>, string>> dependencies;
            if (Mappings.Dependencies.TryGetValue(propertyName, out dependencies))
            {
                //Ok I am interested in this property, but verify it's
                //from the sender I am looking for. We're checking that item.Key
                //is not null here because that signifies this is a dependency
                //on an external object
                foreach (var dependentProperty in
                    dependencies.Where(item=> item.Key != null))
                {
                    object source = this;
                    //Use the stored propertyinfo objects to navigate from our 
                    //object to the source object. If the source property is
                    //Item then we're going to check that it contains the sender
                    if ((source as IList) == null)
                    {
                        foreach (var propertyInfo in dependentProperty.Key.Where(item => item.Name != "Item")/*Path to Change Source*/)
                        {
                            source = propertyInfo.GetValue(source, null);
                        }
                    }

                    var list = source as IList;
                    if ((list != null && list.Contains(sender)) || source == sender)
                    {
                        //It is the sender I want, fire off a property changed
                        OnPropertyChanged(dependentProperty.Value);
                    }
                }
            }
        }

        /// <summary>
        /// Returns a list of currently monitored objects that derive from DependentBindingObject
        /// </summary>
        internal IEnumerable<DependentBindingObject> MonitoredDependentBindingObjects
        {
            get
            {
                return this.notifyChangeMonitor.MonitoredObjects.OfType<DependentBindingObject>();
            }
        }

        /// <summary>
        /// Returns the dependency notifications for this type
        /// </summary>
        private DependencyNotifications Mappings
        {
            get { return typeMappings[this.GetType()]; }
        }

        /// <summary>
        /// When a property is changing notify dependent properties on our object
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="propertyName"></param>
        protected override void OnPropertyChanging(string propertyName)
        {
            base.OnPropertyChanging(propertyName);
            EnumerateDependentProperties(propertyName, (dependentProperty) => { OnPropertyChanging(dependentProperty); });
        }

        /// <summary>
        /// When a property changes on our object, notify dependents properties on our object
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="propertyName">The property that changed</param>
        protected override void OnPropertyChanged(string propertyName)
        {
            base.OnPropertyChanged(propertyName);
            EnumerateDependentProperties(propertyName, (dependentProperty) => { OnPropertyChanged(dependentProperty); });
            OnPropertyNotificationsSent(propertyName);
        }

        /// <summary>
        /// Executes the action for any dependendent property
        /// </summary>
        /// <param name="propertyName">PropertyName to search for</param>
        /// <param name="action">The action that will be called if the name is mapped</param>
        private void EnumerateDependentProperties(string propertyName, Action<string> action)
        {
            List<KeyValuePair<Collection<PropertyInfo>, string>> dependencies;
            if (Mappings.Dependencies.TryGetValue(propertyName, out dependencies))
            {
                foreach (var dependentProperty in dependencies.Where(item => item.Value != propertyName))
                {
                    action(dependentProperty.Value);
                }
            }
        }
    }
}
